同一組 15 題、同一份語料、同一次檢索,我算了兩個分數:
我前幾天都在想辦法把 top-1 從 9 推到 10。今天並排一看才發現,我盯的那個分數
天花板比實際表現還低 5 題。
(語料:虛構的《員工工作規則》59 條。檢索退回 Day 11 的 Chroma、參數凍結,
生成用 gpt-4o-mini、temperature=0。)
它假設看檢索結果的是人。人只想看第一名,所以第一名要對。
但 RAG 的讀者是 LLM,它會把餵進去的 k 段全部讀完,順序幾乎不重要。
假設換了,尺就得換。
| 組別 | top-1 | recall@k | 答案正確 | tokens(輸入/輸出) |
|---|---|---|---|---|
| 完全不給資料 | — | — | 2/15 | 952/856 |
| k=1 | 10/15 | 10/15 | 10/15 | 3649/554 |
| k=3 | 10/15 | 14/15 | 14/15 | 7952/609 |
| k=5 | 10/15 | 14/15 | 15/15 | 12403/613 |
中間兩欄走勢完全一致,而最左邊那欄從頭到尾沒動。
檢索做的事,是把 59 條規章依「跟問題像不像」排名。k 就是我決定拿前幾名
餵給模型。 以第 7 題「特休沒休完會怎樣?」為例,答案寫在第 23 條,
而實際排名是:
第1名 第 22 條(特休假)
第2名 第 26 條(補休)
第3名 第 23 條 ← 答案在這
k=1 只餵第 1 名,模型只看到第 22 條,於是回「規章條文中未提及」;
k=3 餵前三名,第 23 條進去了,它就答得出「遞延至次年三月三十一日」。
同一次檢索、同一題,只因為 k 不一樣,結果就不一樣。
所以兩把尺的差別只有一句話:top-1 問「答案有沒有排第 1 名」,
recall@k 問「答案有沒有擠進前 k 名」,第 2、第 3 名都算。
15 題裡有 10 題的答案排第 1 名(top-1 = 10/15),另外 4 題排第 2、3 名。
k=1 時這 4 題全漏掉,k=3 一次全撿回來(recall@3 = 14/15)。
而 top-1 不管 k 設多少都不會變,它只看第一名——這就是為什麼我調了幾天
它都是 10/15。
每層都要配一句「壞掉時會長什麼樣」——出事的時候你需要的不是分數,
是知道要改哪裡。
| 層 | 指標 | 今天的數字 | 壞掉時的症狀 |
|---|---|---|---|
| 檢索層 | recall@k | 14/15(k=3) | 答案寫「規章條文中未提及」 |
| 生成層 | 答案正確率 | 14/15(k=3) | 條文在 prompt 裡,數字卻抄錯 |
| 歸因層 | 檢索 × 答案交叉表 | 「撈到卻答錯」= 0 | 不知道下一塊錢該花在切塊還是 prompt |
| 成本層 | 輸入/輸出 token | k=3 只花 k=5 的 64% | 分數升 1 題、帳單升 3 倍而你沒發現 |
top-1 不在這張表上。 它是檢索層偏低的代理指標:k=3 時它 10/15、
recall@k 14/15,差 4 題。我優化的是低的那一欄。
每題記兩件事:檢索有沒有撈到、答案對不對。
| 組別 | 撈到且答對 | 撈到但答錯 | 沒撈到卻答對 | 沒撈到且答錯 |
|---|---|---|---|---|
| k=1 | 10 | 0 | 0 | 5 |
| k=3 | 14 | 0 | 0 | 1 |
| k=5 | 14 | 0 | 1 | 0 |
「撈到但答錯」三組都是 0,出乎我意料。只要條文進了 prompt,模型就照抄數字。
而且預期條文排到第 3 名,答對率一樣是 100%。
兩個能直接用的結論:追 recall@k,別追 top-1;
先別急著上 reranker,重排序解的是「排太後面讀不到」,
這問題不存在,k 從 1 拉到 3 就多對 4 題。
所以我另外補了一題要湊兩條才回答得出來的:「我平日加班 3 小時,加班費要
怎麼算?」——第 19 條給加成比例,第 33 條才寫明計算基準是本薪。
| 組別 | 第 19 條 | 第 33 條 | 兩條都到 | 答案正確 |
|---|---|---|---|---|
| k=1 | 第 1 名 | 未進 | ❌ | ❌ |
| k=3 | 第 1 名 | 未進 | ❌ | ✅ |
| k=5 | 第 1 名 | 未進 | ❌ | ✅ |
| k=10 | 第 1 名 | 第 9 名 | ✅ | ✅ |
第 19 條穩坐第一名,第 33 條要 k=10 才擠得進來。
單條 recall 很好看,兩條的聯集很難看。 所以 recall@k 要看的是
「需要的條文有沒有全部到齊」,不是「有沒有到一條」。
但 k=3 就判 ✅ 了。因為第 19 條自己寫著「加班費之計算基準為第 33 條所定之
本薪加計經常性給與」——模型抄到「本薪」兩個字,卻從來沒讀過本薪的定義。
又是一題猜對的(下一節還有一題)。
順帶一提,不給條文的時候它答「基本時薪的 1.5 倍」,我的規章裡沒有這條。
不檢索、不給條文,直接問同樣 15 題,只對 2 題——而那兩題剛好是我的規章跟
勞基法一致的。它答對的不是我的規章。
錯的 13 題不是回答不知道,是掛著「根據公司規章」講錯數字:婚假答五天
(實際八日)、事假答七天(實際十四日)、特休沒休完答「自動作廢」
(實際遞延到次年 3/31)。
從 2/15 到 15/15,中間那 13 題全是檢索換來的。 沒有這組對照,
你分不出分數是資料的功勞,還是模型本來就會。
順帶一提,這組輸出 856 tokens 比 k=5 的 613 還多。沒資料可講的時候它話最多。
判定我用關鍵事實比對:每題先寫下答案必須出現的事實,全中才算對。
不用 LLM 當裁判,因為裁判會偏心、重跑要再付錢、數字不可重現。
{"id": 1, "question": "病假連續請幾天以上需要附診斷證明?",
"expected": "第 24 條", "facts": ["三日", "診斷證明"], "forbid": []}
然後第 9 題「沒來上班也沒跟任何人講,會有什麼後果?」把它玩壞了。
預期第 32 條,facts 只寫了「曠職」一個詞。k=5 撈回第 47、12、49、13、16 條,
第 32 條根本沒進 prompt,模型卻答「將視為曠職,依第八章獎懲規定處理」,
判定 ✅。
但「曠職」在整段 context 只出現一次,在第 16 條(天然災害):
「……公司不得視為曠職。」意思是相反的。模型從「不得視為曠職」生出
「將視為曠職」,結論剛好對。
總表最誠實的寫法是「k=5:14/15,外加 1 題猜中」。
同一題在 k=1/k=3 還藏了另一種失敗:它答「根據第 47 條……」,
第 47 條真的在 prompt 裡而且排第一名,但那條講的是洩漏公司資料的懲處。
引用查得到,內容答非所問——這比憑空掰條號更難抓,因為使用者去查會查到,
而「有附引用」本身就會讓他更相信答案。
所以:分數再高都要抽樣人工複核,而且優先複核答對的題。
答錯的你自然會去看,答對的才是偏誤的藏身處。
檢索的成績單和 RAG 的成績單不是同一張。真正決定答案對不對的是 recall@k,
而我一直看的是低的那一欄。
這次成本:主實驗 60 次生成約新台幣 0.165 元,多條文那題 5 次約 0.025 元。
一份能反覆重跑、逐題留紀錄的評估,成本是兩毛錢,
判定邏輯改幾版都不用重新付錢生成。
還有一件更難受的:今天生成端一題都沒搞砸,代表這 15 題根本不難。
全對的實驗代表考題太簡單。
你的 RAG 上線前,量的是檢索命中率,還是答案正確率?
明天 Day 16:加五題語料裡根本沒有答案的陷阱題,量幻覺率、該拒答時有沒有
拒答、不該拒答時是不是亂拒答,以及引用的條號對不對。
GitHub:https://github.com/wp900622/TrustRAG(day15_rag/)